Java 26일 코스 - Day 10: 접근 제어자와 캡슐화

Day 10: 접근 제어자와 캡슐화

캡슐화는 객체의 내부 데이터를 외부로부터 보호하고, 정해진 방법(메서드)을 통해서만 접근하게 하는 원칙입니다. 마치 자동차 엔진을 직접 만지지 않고 핸들과 페달로 조작하는 것과 같습니다. Java에서는 접근 제어자를 통해 이를 구현합니다.

접근 제어자 4가지

각 접근 제어자의 범위를 이해합니다.

// 파일: AccessModifierDemo.java
public class AccessModifierDemo {
    public String publicField = "누구나 접근 가능";
    protected String protectedField = "같은 패키지 + 자식 클래스";
    String defaultField = "같은 패키지만 접근 가능"; // package-private
    private String privateField = "이 클래스 내부에서만";

    public void publicMethod() {
        System.out.println("public 메서드");
        System.out.println(privateField); // 내부에서는 접근 가능
    }

    protected void protectedMethod() {
        System.out.println("protected 메서드");
    }

    void defaultMethod() {
        System.out.println("default 메서드");
    }

    private void privateMethod() {
        System.out.println("private 메서드");
    }

    // 접근 범위 정리:
    // public    > protected > default > private
    // 어디서든    같은패키지+자식  같은패키지   같은클래스

    public static void main(String[] args) {
        AccessModifierDemo demo = new AccessModifierDemo();
        demo.publicMethod();    // 가능
        demo.protectedMethod(); // 가능 (같은 클래스)
        demo.defaultMethod();   // 가능 (같은 클래스)
        demo.privateMethod();   // 가능 (같은 클래스)
    }
}

Getter/Setter를 활용한 캡슐화

필드를 private으로 숨기고, 공개 메서드를 통해 제어된 접근을 제공합니다.

public class User {
    private String name;
    private String email;
    private int age;
    private String password;

    public User(String name, String email, int age) {
        this.name = name;
        setEmail(email);
        setAge(age);
    }

    // Getter: 값 조회
    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public int getAge() {
        return age;
    }

    // Setter: 값 설정 (유효성 검사 포함)
    public void setName(String name) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("이름은 비어있을 수 없습니다.");
        }
        this.name = name.trim();
    }

    public void setEmail(String email) {
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("유효한 이메일 주소를 입력하세요.");
        }
        this.email = email;
    }

    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("나이는 0~150 사이여야 합니다.");
        }
        this.age = age;
    }

    public void setPassword(String password) {
        if (password.length() < 8) {
            throw new IllegalArgumentException("비밀번호는 8자 이상이어야 합니다.");
        }
        this.password = password;
    }

    // 비밀번호는 getter가 없음 (의도적으로 외부 노출 차단)

    @Override
    public String toString() {
        return "User{name='" + name + "', email='" + email + "', age=" + age + "}";
    }

    public static void main(String[] args) {
        User user = new User("홍길동", "hong@example.com", 25);
        System.out.println(user);

        user.setAge(30);
        System.out.println("변경된 나이: " + user.getAge());

        // user.age = -5; // 컴파일 에러! (private)
        // user.setAge(-5); // 런타임 에러! (유효성 검사)
    }
}

불변 클래스 (Immutable Class)

한 번 생성되면 상태를 변경할 수 없는 안전한 클래스입니다.

public final class Money {
    private final long amount;
    private final String currency;

    public Money(long amount, String currency) {
        if (amount < 0) {
            throw new IllegalArgumentException("금액은 음수일 수 없습니다.");
        }
        this.amount = amount;
        this.currency = currency;
    }

    // Getter만 제공 (Setter 없음)
    public long getAmount() {
        return amount;
    }

    public String getCurrency() {
        return currency;
    }

    // 상태 변경 대신 새 객체 반환
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("통화가 다릅니다.");
        }
        return new Money(this.amount + other.amount, this.currency);
    }

    public Money subtract(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("통화가 다릅니다.");
        }
        return new Money(this.amount - other.amount, this.currency);
    }

    @Override
    public String toString() {
        return amount + " " + currency;
    }

    public static void main(String[] args) {
        Money price = new Money(50000, "KRW");
        Money tax = new Money(5000, "KRW");
        Money total = price.add(tax);

        System.out.println("가격: " + price);   // 50000 KRW
        System.out.println("세금: " + tax);     // 5000 KRW
        System.out.println("합계: " + total);   // 55000 KRW
        // price 객체는 변경되지 않음 (불변)
    }
}

record 클래스 (Java 16+)

불변 데이터 클래스를 간결하게 만드는 문법입니다.

// record: 자동으로 생성자, getter, equals, hashCode, toString 생성
public record Product(String name, int price, String category) {

    // 컴팩트 생성자: 유효성 검사
    public Product {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("상품명은 필수입니다.");
        }
        if (price < 0) {
            throw new IllegalArgumentException("가격은 음수일 수 없습니다.");
        }
    }

    // 추가 메서드 정의 가능
    public int discountedPrice(int percent) {
        return price - (price * percent / 100);
    }

    public String displayInfo() {
        return String.format("[%s] %s - %,d원", category, name, price);
    }

    public static void main(String[] args) {
        Product laptop = new Product("MacBook Pro", 2500000, "전자기기");
        Product phone = new Product("Galaxy S24", 1200000, "전자기기");

        // getter는 필드명과 동일 (getXxx 아님)
        System.out.println("상품명: " + laptop.name());
        System.out.println("가격: " + laptop.price());
        System.out.println(laptop.displayInfo());
        System.out.println("10% 할인: " + laptop.discountedPrice(10) + "원");

        // 자동 생성된 toString
        System.out.println(phone);

        // 자동 생성된 equals
        Product another = new Product("MacBook Pro", 2500000, "전자기기");
        System.out.println(laptop.equals(another)); // true
    }
}

오늘의 연습문제

  1. 은행 계좌 캡슐화: BankAccount 클래스를 캡슐화 원칙에 따라 재설계하세요. 잔액은 직접 접근 불가, 입금/출금은 메서드를 통해서만 가능하며, 출금 시 잔액 부족이면 예외를 던지세요.

  2. 학생 성적 관리: StudentGrade 클래스를 만드세요. 이름과 과목별 점수(배열)를 private으로 관리하고, 평균 계산, 최고/최저 점수 조회 메서드를 제공하세요. 점수는 0~100 범위만 허용합니다.

  3. record 활용: Address record를 만들어 우편번호, 시/도, 구/군, 상세주소를 저장하고, fullAddress() 메서드로 전체 주소를 문자열로 반환하세요. 우편번호 형식 검증을 컴팩트 생성자에 추가하세요.

이 글이 도움이 되었나요?